package timbap.visualizer.impl.context.textures;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.farng.mp3.AbstractMP3FragmentBody;
import org.farng.mp3.MP3File;
import org.farng.mp3.TagException;
import org.farng.mp3.id3.AbstractID3v2;
import org.farng.mp3.id3.AbstractID3v2Frame;
import processing.core.PApplet;
import processing.core.PImage;
import timbap.visualizer.api.context.Configuration;
import timbap.visualizer.api.context.playlist.PlaylistManager;
import timbap.visualizer.api.context.textures.LRUCache;
import timbap.visualizer.api.context.textures.TextureCacheManager;
import timbap.visualizer.api.model.ArtworkData;
import timbap.visualizer.api.model.PlaylistEntry;

import java.awt.Image;
import java.awt.MediaTracker;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public abstract class AbstractP5TextureCacheManager implements TextureCacheManager {

	private static final Log LOG = LogFactory.getLog(AbstractP5TextureCacheManager.class);

    private static final String mp3ExtensionRegex = "\\.[mM][pP]3$";
    private static final String[] extensionReplacementsToCheck =
            {".jpg", ".JPG", ".gif", ".GIF", ".png", ".PNG"};
    private static final String[] folderArtworkFilenamesToCheck =
            {"folder.jpg", "Folder.jpg", "FOLDER.JPG", "Folder.JPG", "folder.JPG",
             "cover.jpg", "Cover.jpg", "COVER.JPG", "Cover.JPG", "cover.JPG"};

	private static final String PICTURE_TAG = "APIC\0";
	private static final String PICTURE_DATA_TAG = "Picture Data";
	private static final String MIME_TAG = "MIME Type";
	
	private PApplet p5Applet;
    private Configuration cfg;
    private PlaylistManager playlistManager;
    private MediaTracker syncImageLoaderTracker;
    private Map<ArtworkData,ImageLoaderThread> asyncImageLoaderMap = new HashMap<ArtworkData,ImageLoaderThread>();

    private Map<PlaylistEntry,ArtworkData> artworkDataMap = new HashMap<PlaylistEntry,ArtworkData>();
    private LRUCache<PlaylistEntry, PImage> textureCache;

    private ArtworkData defaultArtworkData;
    private PImage defaultArtworkTexture;

    public AbstractP5TextureCacheManager(PApplet pP5Applet, Configuration pCfg, PlaylistManager pPlaylistManager) {
        
    	p5Applet = pP5Applet;
        cfg = pCfg;
        playlistManager = pPlaylistManager;
        syncImageLoaderTracker = new MediaTracker(p5Applet);
        try {
        	defaultArtworkData = new ArtworkData(cfg.getDefaultArtworkFile());
            defaultArtworkTexture = createImage(defaultArtworkData);
        } catch (Exception e) {
        	LOG.error("Could not find default artwork file " + cfg.getDefaultArtworkFile());
        }
        textureCache = new LRUCache<PlaylistEntry,PImage>(cfg.getTextureCacheSize());
    }

    public void fillInitialTextureCache() {
    	for (int i=0; i < playlistManager.getPlaylistSize(); i++) {
            PlaylistEntry tPlaylistEntry = playlistManager.getPlaylistEntry(i);
            loadArtworkData(tPlaylistEntry);
            if (i < cfg.getTextureCacheSize()) {
                putArtworkTexture(tPlaylistEntry);
            }
        }
    	
    }

    public PImage getArtworkTextureAsync(PlaylistEntry pPlaylistEntry) {
    	PImage tArtworkTexture = getArtworkTextureFromCache(pPlaylistEntry);
        if (tArtworkTexture == null) {
            ArtworkData tArtworkData = getArtworkData(pPlaylistEntry);
            if (!asyncImageLoaderMap.containsKey(tArtworkData)) {
                tArtworkTexture = putArtworkTexture(pPlaylistEntry);
            } else if (asyncImageLoaderMap.get(tArtworkData).isLoaded()) {
                tArtworkTexture = putArtworkTexture(pPlaylistEntry);
            }
        }
        if (tArtworkTexture == null) {
            return defaultArtworkTexture;
        }
        return tArtworkTexture;
    }

    public PImage getArtworkTextureInSync(PlaylistEntry pPlaylistEntry) {
        PImage tArtworkTexture = getArtworkTextureFromCache(pPlaylistEntry);
        if (tArtworkTexture == null) {
            ArtworkData tArtworkData = getArtworkData(pPlaylistEntry);
            if (!asyncImageLoaderMap.containsKey(tArtworkData)) {
                tArtworkTexture = putArtworkTexture(pPlaylistEntry);
            }
            AbstractP5TextureCacheManager.ImageLoaderThread tLoaderThread =
                    asyncImageLoaderMap.get(tArtworkData);
            if (tLoaderThread != null) {
                try {
                    // wait for loader to finish
                    tLoaderThread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (tLoaderThread.isLoaded()) {
                    tArtworkTexture = tLoaderThread.getLoadedImage();
                }
            }
        }
        if (tArtworkTexture == null) {
            return defaultArtworkTexture;
        }
        return tArtworkTexture;
    }

    private PImage getArtworkTextureFromCache(PlaylistEntry pPlaylistEntry) {
    	return textureCache.get(pPlaylistEntry);
    }
    
    public PImage putArtworkTexture(PlaylistEntry pPlaylistEntry) {
    	PImage tArtworkTexture = loadArtworkTexture(pPlaylistEntry);
	    if (tArtworkTexture != null) {
	    	textureCache.put(pPlaylistEntry, tArtworkTexture);
	    }
	    return tArtworkTexture;
    }
    
    protected PImage loadArtworkTexture(PlaylistEntry pPlaylistEntry) {
    	PImage tLoadedArtworkTexture = null;
    	if (pPlaylistEntry != null) {
            ArtworkData tArtworkData = getArtworkData(pPlaylistEntry);
            if (tArtworkData != null && tArtworkData.equals(defaultArtworkData)) {
                return defaultArtworkTexture;
            }
            try {
                tLoadedArtworkTexture = createImageAsync(tArtworkData);
                if (tLoadedArtworkTexture != null) {
                    LOG.debug("Parsing artwork from internal buffer succeeded for '" + pPlaylistEntry.getAudioFile() + "'.");
                }
            } catch (Exception e) {
                LOG.warn("Parsing artwork from internal buffer failed: " + e.getMessage());
            }

        }
    	return tLoadedArtworkTexture;
    }

    class ImageLoaderThread extends Thread {

        private ArtworkData artworkData;
        private PImage loadedImage;
        private boolean loaded = false;
        private boolean failed = false;
        private long startTime;

        ImageLoaderThread(ArtworkData pArtworkData) {
            artworkData = pArtworkData;
        }

        public synchronized boolean isLoaded() {
            return loaded;
        }

        public boolean isFailed() {
            return failed;
        }

        public synchronized PImage getLoadedImage() {
            return loadedImage;
        }

        public long getStartTime() {
            return startTime;
        }

        public void run() {

            startTime = System.currentTimeMillis();
            Image tAwtImage = null;
            try {
                tAwtImage = artworkData.toImage();
            } catch (IOException e) {
                e.printStackTrace();
            }
            syncImageLoaderTracker.addImage(tAwtImage,(int)this.getId());
            try {
                long tStart = System.currentTimeMillis();
                syncImageLoaderTracker.waitForID((int)this.getId(), 2000);
                syncImageLoaderTracker.removeImage(tAwtImage);
                loadedImage = new PImage(tAwtImage);
                loaded = true;
                LOG.debug("image loading took " + (System.currentTimeMillis() - tStart) + " ms");
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Successfully created new image.");
                }
            } catch (InterruptedException e) {
                syncImageLoaderTracker.removeImage(tAwtImage);
                LOG.warn("Creating image failed. Tracker was interrupted.");
            } finally {
                if (!loaded) {
                    failed = true;
                }
            }
        }

    }

    private PImage createImageAsync(ArtworkData pArtworkData) throws IOException {
        long tStart = System.currentTimeMillis();
        PImage tReturnImage = null;
        AbstractP5TextureCacheManager.ImageLoaderThread tImgLoaderThread = asyncImageLoaderMap.get(pArtworkData);
        if (tImgLoaderThread != null) {
            if (tImgLoaderThread.isLoaded() || tImgLoaderThread.isFailed()) {
                tReturnImage = tImgLoaderThread.getLoadedImage();
                asyncImageLoaderMap.remove(pArtworkData);
            } else {
                if (System.currentTimeMillis() - tImgLoaderThread.getStartTime() > 5000) {
                    tImgLoaderThread.stop();
                    asyncImageLoaderMap.remove(pArtworkData);
                }
            }
        } else {
            tImgLoaderThread = new ImageLoaderThread(pArtworkData);
            asyncImageLoaderMap.put(pArtworkData, tImgLoaderThread);
            tImgLoaderThread.start();
            LOG.debug("async image loader returned after " + (System.currentTimeMillis() - tStart) + " ms");
        }
        return tReturnImage;
    }

    private PImage createImage(ArtworkData pArtworkData) throws IOException {
        long tStart = System.currentTimeMillis();

        PImage tCreatedImage = null;

        Image tAwtImage = pArtworkData.toImage();
        syncImageLoaderTracker.addImage(tAwtImage, 0);
        try {
            syncImageLoaderTracker.waitForID(0);
            syncImageLoaderTracker.removeImage(tAwtImage);
            tCreatedImage = new PImage(tAwtImage);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Successfully created new image.");
            }
        } catch (InterruptedException e) {
            syncImageLoaderTracker.removeImage(tAwtImage);
            LOG.warn("Creating image failed. Tracker was interrupted.");
        }
        LOG.debug("creating image synchronously took " + (System.currentTimeMillis() - tStart) + " ms");

        return tCreatedImage;
	}


    public ArtworkData getArtworkData(PlaylistEntry pPlaylistEntry) {
        ArtworkData tArtworkData = artworkDataMap.get(pPlaylistEntry);
        return (tArtworkData != null) ? tArtworkData : defaultArtworkData; 
    }



    private ArtworkData loadArtworkData(PlaylistEntry pPlaylistEntry) {
		ArtworkData tArtworkData = null;
		try {
			tArtworkData = loadArtworkDataFromArtworkFile(pPlaylistEntry);
		} catch (Exception e) {
			LOG.warn("Loading artwork from companion artwork file of " + pPlaylistEntry.getAudioFile() + " failed: " + e.getMessage());
		}
		if (tArtworkData == null) {
			try {
				tArtworkData = loadArtworkDataFromID3v2Tag(pPlaylistEntry);
			} catch (Exception e) {
				LOG.warn("Loading artwork from ID3v2 tag of " + pPlaylistEntry.getAudioFile() + " failed: " + e.getMessage());
			}
		}
        artworkDataMap.put(pPlaylistEntry, tArtworkData);
		return tArtworkData;
	}
    
    private ArtworkData loadArtworkDataFromArtworkFile(PlaylistEntry pPlaylistEntry) throws IOException {
    	ArtworkData tArtworkData = null;
		File tArtworkFile = findArtworkFile(pPlaylistEntry);
		if (tArtworkFile != null) {
			tArtworkData = new ArtworkData(tArtworkFile);
		}
		return tArtworkData;
	}

	private ArtworkData loadArtworkDataFromID3v2Tag(PlaylistEntry pPlaylistEntry) throws TagException, IOException {
    	ArtworkData tArtworkData = null;
		MP3File tMp3file = new MP3File(pPlaylistEntry.getAudioFile());
		AbstractID3v2 tId3v2 = tMp3file.getID3v2Tag();
		if (tId3v2 != null) {
			AbstractID3v2Frame tApicFrame = tId3v2.getFrame(PICTURE_TAG);
			if (tApicFrame != null) {
				AbstractMP3FragmentBody tApicFrameBody = tApicFrame.getBody();
				String tMimeType = (String) tApicFrameBody.getObject(MIME_TAG);
				Object tBytesAsObject = tApicFrameBody.getObject(PICTURE_DATA_TAG);
				if (tBytesAsObject != null) {
					tArtworkData = new ArtworkData((byte[])tBytesAsObject, tMimeType);
				}
			}
		}
		return tArtworkData;
	}

    public static File findOrCreateArtworkFile(PlaylistEntry pPlaylistEntry, String pExtension) {
        String tAudioFilePath = pPlaylistEntry.getAudioFile().getAbsolutePath();
        return new File(tAudioFilePath.replaceFirst(mp3ExtensionRegex, pExtension));
    }

    public static File findArtworkFile(PlaylistEntry pPlaylistEntry) {
    	File tArtworkFile = null;
    	String tAudioFilePath = pPlaylistEntry.getAudioFile().getAbsolutePath();
        for (String tExtensionReplacementToCheck : extensionReplacementsToCheck) {
            tArtworkFile = new File(
                    tAudioFilePath.replaceFirst(mp3ExtensionRegex, tExtensionReplacementToCheck));
            if (tArtworkFile.canRead() && tArtworkFile.length() > 0) {
                break;
            }
        }
    	if (tArtworkFile == null) {
            for (String tFolderArtworkFilenameToCheck : folderArtworkFilenamesToCheck) {
                tArtworkFile = new File(pPlaylistEntry.getAudioFile().getParentFile(), tFolderArtworkFilenameToCheck);
                if (tArtworkFile.canRead() && tArtworkFile.length() > 0) {
                    break;
                }
            }
    	}
        return tArtworkFile;
    }
	
    protected PApplet getP5Applet() {
		return p5Applet;
	}

	protected PlaylistManager getPlaylistManager() {
		return playlistManager;
	}

	protected Configuration getCfg() {
		return cfg;
	}

}
